package com.mezz.bones.im.gateway.service.impl;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.mezz.bones.im.common.domain.user.ImUserAuthInfo;
import com.mezz.bones.im.common.result.ImResultCode;
import com.mezz.bones.im.common.exception.ImBaseException;
import com.mezz.bones.im.common.message.base.Message;
import com.mezz.bones.im.common.message.base.MessageHeader;
import com.mezz.bones.im.common.domain.user.ImUserConnectInfo;
import com.mezz.bones.im.common.result.ImResult;
import com.mezz.bones.im.common.service.IAuthService;
import com.mezz.bones.im.gateway.service.IUserStatusService;
import com.mezz.bones.im.gateway.manager.UserConnectInfoManager;
import com.mezz.bones.im.gateway.manager.UserIpManager;
import com.mezz.bones.im.gateway.service.IUserChannelService;
import com.mezz.bones.im.gateway.util.ChannelUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: zhuang
 * @Date: 2024/12/6 17:14
 * @Description: TODO
 * @Version: 1.0
 **/

@Service
@Slf4j
public class UserChannelServiceImpl implements IUserChannelService {

    @Resource
    private IUserStatusService userStatusService;

    @Resource
    private UserConnectInfoManager userConnectInfoManager;

    @Resource
    private UserIpManager userIpManager;

    @Resource
    private IAuthService authService;
    private static final Map<ImUserConnectInfo, Channel> USER_CHANNEL_MAP = new ConcurrentHashMap<>();
    
    /**
     * 未登录连接管理
     */
    private static final Map<String, ChannelHandlerContext> UNAUTH_CHANNEL_MAP = new ConcurrentHashMap<>();

    /**
     * 添加未认证的连接
     * @param ctx 连接上下文
     */
    @Override
    public void addUnAuthChannel(ChannelHandlerContext ctx) {
        String channelId = ctx.channel().id().asLongText();
        UNAUTH_CHANNEL_MAP.put(channelId, ctx);
        log.debug("添加未认证连接, channelId: {}, 当前未认证连接数: {}", channelId, UNAUTH_CHANNEL_MAP.size());
    }

    /**
     * 移除未认证的连接
     * @param ctx 连接上下文
     */
    @Override
    public void removeUnAuthChannel(ChannelHandlerContext ctx) {
        String channelId = ctx.channel().id().asLongText();
        UNAUTH_CHANNEL_MAP.remove(channelId);
        log.debug("移除未认证连接, channelId: {}, 当前未认证连接数: {}", channelId, UNAUTH_CHANNEL_MAP.size());
    }

    /**
     * 用户上线
     * @param userInfo
     */
    private void bindChannel(ImUserConnectInfo userInfo, ChannelHandlerContext ctx){
        //map 新增
        bind2Map(userInfo,ctx.channel());
        //用户在线
        String ip = userIpManager.getIp(ctx);
        userStatusService.online(userInfo,ctx, ip);
    }

    @Override
    public void initOnLine(ImUserConnectInfo imUserConnectInfo, ChannelHandlerContext ctx) {

        //保存客户端连接信息到channel上下文 连接断开后 内存信息回自动释放
        userConnectInfoManager.setUserConnectInfo(ctx, imUserConnectInfo);

        //绑定channel信息
        bindChannel(imUserConnectInfo,ctx);

        log.info("当前连接数量:{}",USER_CHANNEL_MAP.size());

    }

    @Override
    public Channel getChannelByUserInfoFromMap(ImUserConnectInfo userInfo){

        Channel channel = USER_CHANNEL_MAP.get(userInfo);
        if(channel == null){
            //处理redis上的存储数据 保证一致性
            //一般不会走到这个逻辑  除非 服务器端重启后 连接丢失 但是redis状态一样有效
            if(userStatusService.isOnline(userInfo)){
                //如果是在线状态 则更新状态为下线
                userStatusService.offline(userInfo);
            }
        }

        return channel;
    }

    /**
     * 获取所有用户的有效连接
     * @param userId
     * @return
     */
    @Override
    public List<Channel> getChannelByUserId(String userId) {
        return null;
    }

    private void bind2Map(ImUserConnectInfo userInfo, Channel channel) {

        // 之前已经绑定过了，移除并释放掉之前绑定的channel
        if (USER_CHANNEL_MAP.containsKey(userInfo)) {
            Channel oldChannel = getChannelByUserInfoFromMap(userInfo);
            if(oldChannel != null){
                //关闭旧的连接通道
                ChannelUtil.sendAndGracefullyClose(oldChannel, ImResult.errorMsg(StrUtil.format("移除相同设备上的其它连接通道 RemovedUserInfo: {}", JSONObject.toJSONString(userInfo))));
            }

            try {
                //等待此连接关闭 有异步操作 防止map中同一对象被重复删除
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        // userChannelKey -> channel
        USER_CHANNEL_MAP.put(userInfo, channel);

    }

    /**
     * 连接下线 close事件触发
     * 1. 用户连接主动断开
     * 2. 用户设置主动退出 logout
     * 3. 用户连接心跳超时
     * @param userInfo
     */
    @Override
    public void forceOffLine(ImUserConnectInfo userInfo){

        if(userInfo == null){
            log.debug("连接还未绑定用户信息-未通过握手认证阶段");
            return;
        }

        //从map中移除
        Channel channel = USER_CHANNEL_MAP.remove(userInfo);

        //离线
        userStatusService.offline(userInfo);

        log.info("当前节点连接数:{}",onlineCount());

        if(channel != null){
            channel.close();
        }
    }

    @Override
    public void validMsgData(Message message) {

        validMsgHeader(message.getHeader());

        // Optionally validate the message body if it's required
        if (message.getBody() == null) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "Message body is missing");
        }
    }

    @Override
    public void validMsgHeader(MessageHeader header) {

        if(header == null){
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "Message header is required");
        }

        // Validate required fields in the header
        if (StrUtil.isBlank(header.getActionCode())) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "Action code is required");
        }

        if (StrUtil.isBlank(header.getTraceId())) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "TraceId is required");
        }

        if (header.getTimestamp() == null) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "Timestamp is required");
        }

        if (StrUtil.isBlank(header.getDeviceType())) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "DeviceType is required");
        }

        if (!header.getDeviceType().matches("ios|android|miniapp|pc")) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "DeviceType supports [ios|android|miniapp|pc]");
        }

        if(StrUtil.isBlank(header.getImei())){
            log.debug("客户端设备号为空");
            //默认给一个设备号字符串信息
            header.setImei("none");
        }
    }

    @Override
    public ImUserConnectInfo authUserInfo(ChannelHandlerContext ctx, MessageHeader header) {

        String ip = userIpManager.getIp(ctx);
        String token = header.getToken();
        if(StrUtil.isBlank(token)) {
            throw new ImBaseException(ImResultCode.PARAM_ERROR, "token is required");
        }

        ImUserAuthInfo imUserAuthInfo = authService.authUser(token);
        String userId = imUserAuthInfo.getUserId();
        String userType = imUserAuthInfo.getUserType();

        //保存channel 用户信息到上下文
        return ImUserConnectInfo.builder()
                .userId(userId)
                .userType(userType)
                .imei(header.getImei())
                .deviceType(header.getDeviceType())
                .ip(ip)
                .build();

    }

    @Override
    public Integer onlineCount(){
        return USER_CHANNEL_MAP.size();
    }

    /**
     * 获取所有连接信息
     * @return
     */
    @Override
    public Map<ImUserConnectInfo, Channel> allChannel(){
        return USER_CHANNEL_MAP;
    }

    /**
     * 获取所有用户的连接信息
     * @param userId
     * @return
     */
    @Override
    public List<ImUserConnectInfo> allUserChannel(String userId){

        List<ImUserConnectInfo> result= new ArrayList<>();

        for (ImUserConnectInfo imUserConnectInfo : allChannel().keySet()) {

            if(imUserConnectInfo.getUserId().equals(userId)){
                result.add(imUserConnectInfo);
            }
        }
        return result;
    }
}
